/* 
 * [New BSD License (BSD 3-Clause License)]
 * Copyright (c) 2012, Max Bechtold
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     - Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     - Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     - Neither the name of the Rel2Xml Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package maxbe.rel2xml.generator.docs.constraints;

import java.util.ArrayList;
import java.util.List;

import maxbe.rel2xml.generator.Table;
import maxbe.rel2xml.grammar.FillTableException;

import org.apache.log4j.Logger;

/**
 * Main class for constraints on column values, consisting of one main
 * constraint (i.e. ValueConstructor) and maybe some partial constraints
 * 
 * @author Max Bechtold
 * 
 */
public class DataConstraint {

	private static Logger log = Logger.getLogger(DataConstraint.class);

	private final ValueConstructor valueConstructor;

	private PartialConstraint pcOnFirst;
	private PartialConstraint pcOnLast;
	private PartialConstraint pcOnRandom;

	/** Specifies the context the PartialConstraints are defined on */
	private Table partialContext;

	/** Number of so far generated tuples */
	private int count = 0;

	/**
	 * Total number of values to be generated in this context (or overall, if
	 * global == true)
	 */
	private int total;

	/** Number of in preceding contexts generated values */
	private int startOff;

	public DataConstraint(ValueConstructor mainConstraint) {
		this.valueConstructor = mainConstraint;
	}

	public ValueConstructor getValueConstructor() {
		return valueConstructor;
	}

	public Table getPartialContext() {
		return partialContext;
	}

	public PartialConstraint getPcOnFirst() {
		return pcOnFirst;
	}

	public PartialConstraint getPcOnLast() {
		return pcOnLast;
	}

	public PartialConstraint getPcOnRandom() {
		return pcOnRandom;
	}

	public void setPartialContext(Table partialContext) {
		this.partialContext = partialContext;

		if (valueConstructor instanceof UniqueNumbers) {
			UniqueNumbers un = (UniqueNumbers) valueConstructor;
			if (un.getUniqueContext() != null && partialContext != null
					&& un.getUniqueContext() != partialContext) {
				String msg = String
						.format("The PartialConstraints of ValueConstructor '%s' have defined a context differing from that of the ValueConstructor.",
								valueConstructor);
				log.error(msg);
				throw (new FillTableException(msg));
			}
		} else if (valueConstructor instanceof DerivedValues) {
			DerivedValues dv = (DerivedValues) valueConstructor;
			if (!dv.isLocal() && partialContext != null
					&& dv.getSourceColumn().getTable() != partialContext) {
				String msg = String
						.format("The PartialConstraints of ValueConstructor '%s' have defined a context differing from that of the ValueConstructor.",
								valueConstructor);
				log.error(msg);
				throw (new FillTableException(msg));
			}
		}
	}

	public void setPcOnRandom(PartialConstraint pcOnRandom) {
		this.pcOnRandom = pcOnRandom;
		checkPercentage();
	}

	public void setPcOnFirst(PartialConstraint pcOnFirst) {
		this.pcOnFirst = pcOnFirst;
		checkPercentage();
	}

	public void setPcOnLast(PartialConstraint pcOnLast) {
		this.pcOnLast = pcOnLast;
		checkPercentage();
	}

	public void announce(int number) {
		total = number;

		startOff = 0;

		valueConstructor.announce(number);
		if (pcOnFirst != null) {
			pcOnFirst.announce(number);
		}
		if (pcOnRandom != null) {
			pcOnRandom.announce(number);
		}
		if (pcOnLast != null) {
			pcOnLast.announce(number);
		}
	}

	public void mark(int number) {
		valueConstructor.mark(number);

		if (pcOnFirst != null && !pcOnFirst.isGlobal()) {
			pcOnFirst.getValueConstructor().mark(number);
			total = count + number;
			startOff = count;
			if (pcOnFirst.derivesBoundary())
				pcOnFirst.mark(number);
		}

		if (pcOnRandom != null && !pcOnRandom.isGlobal()) {
			pcOnRandom.getValueConstructor().mark(number);
		}

		if (pcOnLast != null && !pcOnLast.isGlobal()) {
			pcOnLast.getValueConstructor().mark(number);
			total = count + number;
			startOff = count;
			if (pcOnLast.derivesBoundary())
				pcOnLast.mark(number);
		}
	}

	public List<String> next(int number) {
		List<String> result = new ArrayList<String>();
		int end = count + number;

		// Generate values by requesting them from pcOnFirst
		if (pcOnFirst != null && count < startOff + pcOnFirst.getBoundary()) {
			int fromFirstPc = startOff + pcOnFirst.getBoundary() - count;
			if (fromFirstPc > number) {
				fromFirstPc = number;
			}

			result.addAll(pcOnFirst.next(fromFirstPc));
			count += fromFirstPc;
		}

		// Generate values by alternating between pcOnRandom (if existent) and
		// valueConstructor
		int rest = end - count;
		if (rest > 0) {
			int offset = 0;
			int fromVc = rest;
			int fromRandomPc = 0;

			if (pcOnRandom != null) {
				offset = pcOnRandom.calcNextOffset(rest);
				fromVc = offset - 1;
				fromRandomPc = 1;
			}

			// Only one iteration if pcOnRandom == null, else possibly more
			while (rest > 0
					&& (pcOnLast == null || count < total
							- pcOnLast.getBoundary())) {
				if (fromVc >= rest) {
					fromVc = rest;
					fromRandomPc = 0;
				}
				if (pcOnLast != null
						&& count + fromVc + fromRandomPc > total
								- pcOnLast.getBoundary()) {
					if (fromVc > 0) {
						// else: only one value to be generated by randomPc
						fromVc = total - pcOnLast.getBoundary() - count;
						fromRandomPc = 0;
					}
				}

				result.addAll(valueConstructor.next(fromVc));
				if (fromRandomPc == 1) {
					result.add(pcOnRandom.next());
				}
				count += fromVc + fromRandomPc;
				rest = end - count;

				if (pcOnRandom != null && rest > 0) {
					offset = pcOnRandom.calcNextOffset(rest);
					fromVc = offset - 1;
				}
			}
		}

		// Generate values by requesting them from pcOnLast
		if (count < end && pcOnLast != null) {
			//
			int fromLastPc = pcOnLast.getBoundary();
			if (fromLastPc > end - count) {
				fromLastPc = end - count;
			}

			// Generate values and attach to result
			result.addAll(pcOnLast.next(fromLastPc));
			count += fromLastPc;
		}

		return result;

	}

	@Override
	public String toString() {
		String result = valueConstructor.toString();
		String pcs = "";
		if (pcOnFirst != null) {
			pcs += pcOnFirst.toString();
		}
		if (pcOnRandom != null) {
			if (pcs.length() > 0) {
				pcs += ", ";
			}
			pcs += pcOnRandom.toString();
		}
		if (pcOnLast != null) {
			if (pcs.length() > 0) {
				pcs += ", ";
			}
			pcs += pcOnLast.toString();
		}
		if (pcs.length() > 0) {
			result += " with"
					+ (partialContext != null ? " per "
							+ partialContext.getName() : "") + ' ' + pcs;
		}
		return result;
	}

	public boolean needsAnnounce() {
		// First and last PartialConstraint need announcement as they have to
		// compute their count field
		boolean needsAnnounce = valueConstructor.needsAnnounce();
		if (pcOnFirst != null && pcOnFirst.isGlobal()) {
			needsAnnounce = true;
		}
		if (pcOnRandom != null) {
			needsAnnounce = needsAnnounce || pcOnRandom.needsAnnounce();
		}
		if (pcOnLast != null && pcOnLast.isGlobal()) {
			needsAnnounce = true;
		}
		return needsAnnounce;
	}

	private void checkPercentage() {

		int sum = 0;

		float scale = 1;
		if (pcOnFirst != null) {
			sum += pcOnFirst.getPercentage();
		}

		if (pcOnLast != null) {
			sum += pcOnLast.getPercentage();
		}

		if (pcOnRandom != null) {
			scale = (float) (1 - (sum / 100.0));
			pcOnRandom.setEffPercentage(pcOnRandom.getPercentage() / scale);
			sum += pcOnRandom.getPercentage();
		}

		if (sum > 100) {
			String msg = String
					.format("A PartialConstraint of ValueConstructor '%s'  has a percentage exceeding 100.",
							valueConstructor);
			log.error(msg);
			throw (new FillTableException(msg));
		}

	}

}
